package luccy.desc.impl;

import java.lang.annotation.Annotation;
import java.lang.annotation.ElementType;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import luccy.desc.AnnotationDesc;
import luccy.desc.BeanDesc;
import luccy.desc.ClassDesc;
import luccy.desc.ConstructorDesc;
import luccy.desc.Desc;
import luccy.desc.MethodDesc;
import luccy.desc.PackageDesc;
import luccy.desc.PackageDescHolder;
import luccy.desc.PropertyDesc;
import luccy.util.AssertionUtil;
import luccy.util.ConstructorUtil;
import luccy.util.JavaBeansUtil;
import luccy.util.PackageUtil;

public class BeanDescImpl<T> implements BeanDesc<T> {

	private ClassDesc<T> classDesc;

	private ConstructorDesc constructorDesc;

	private Map<ElementType, Map<Desc, List<AnnotationDesc>>> annotationDescMap = new HashMap<ElementType, Map<Desc, List<AnnotationDesc>>>();

	private Map<String, PropertyDesc> propertyDescMap = new HashMap<String, PropertyDesc>();

	private List<PropertyDesc> propertyDescList = new ArrayList<PropertyDesc>();

	private Map<String, List<MethodDesc>> methodDescMap = new HashMap<String, List<MethodDesc>>();

	private List<MethodDesc> methodDescList = new ArrayList<MethodDesc>();

	public BeanDescImpl(Class<T> componentClass) {
		this(componentClass, null);
	}

	public BeanDescImpl(Class<T> componentClass,
			PackageDescHolder packageDescHolder) {
		AssertionUtil.assertNotNull(componentClass);
		analyzeClasses(componentClass);
		analyzeConstructors();
		analyzePropertiesAndMethods(componentClass);
		analyzeAnnotations(componentClass);
		if (packageDescHolder != null) {
			analyzePackages(packageDescHolder);
		}
	}

	protected void analyzePackages(PackageDescHolder packageDescHolder) {
		final Class<T> c = getClassDesc().getComponentClass();
		final String pkgname = PackageUtil.getPackage(c);
		PackageDesc packageDesc = packageDescHolder.getPackageDesc(pkgname);
		if (packageDesc == null) {
			packageDesc = new PackageDescImpl(this);
		}
		packageDesc.addBeanDesc(this);
	}

	protected void analyzePropertiesAndMethods(final Class<T> componentClass) {
		for (Method m : componentClass.getMethods()) {
			final String methodName = m.getName();
			if (JavaBeansUtil.isGetMethod(methodName, m)) {
				String propertyName = JavaBeansUtil.decapitalize(methodName
						.substring(3));
				setReadMethod(m, propertyName);
			} else if (JavaBeansUtil.isIsMethod(methodName, m)) {
				String propertyName = JavaBeansUtil.decapitalize(methodName
						.substring(2));
				setReadMethod(m, propertyName);
			} else if (JavaBeansUtil.isSetMethod(methodName, m)) {
				String propertyName = JavaBeansUtil.decapitalize(methodName
						.substring(3));
				setWriteMethod(m, propertyName);
			} else {
				addMethodDesc(new MethodDescImpl(m));
			}
		}
	}

	protected void setReadMethod(Method readMethod, String propertyName) {
		PropertyDesc propertyDesc = getPropertyDesc(propertyName);
		if (propertyDesc == null) {
			propertyDesc = new PropertyDescImpl(getClassDesc(), propertyName);
			Class propertyType = readMethod.getReturnType();
			propertyDesc.setPropertyType(propertyType);
			addPropertyDesc(propertyName, propertyDesc);
		}
		propertyDesc.setPropertyName(propertyName);
		propertyDesc.setReadMethod(readMethod);
	}

	protected void setWriteMethod(Method writeMethod, String propertyName) {
		PropertyDesc propertyDesc = getPropertyDesc(propertyName);
		if (propertyDesc == null) {
			propertyDesc = new PropertyDescImpl(getClassDesc(), propertyName);
			final Class<?> propertyType = writeMethod.getParameterTypes()[0];
			propertyDesc.setPropertyType(propertyType);
			addPropertyDesc(propertyName, propertyDesc);
		}
		propertyDesc.setPropertyName(propertyName);
		propertyDesc.setWriteMethod(writeMethod);
	}

	protected void analyzeAnnotations(final Class<T> componentClass) {
		final AnnotationDescHandler classAnnoHandler = new AnnotationDescHandler() {

			public void handle(BeanDesc beanDesc, Class c) {
				final Annotation[] annotations = c.getDeclaredAnnotations();
				addAnnotationDescs(annotations, beanDesc.getClassDesc(),
						ElementType.TYPE);
			}

		};
		final AnnotationDescHandler methodAnnoHandler = new AnnotationDescHandler() {

			public void handle(BeanDesc beanDesc, Class c) {
				for (Method m : c.getDeclaredMethods()) {
					final Annotation[] annotations = m.getDeclaredAnnotations();
					final String name = m.getName();
					final MethodDesc methodDesc = beanDesc.getMethodDesc(name);
					if (JavaBeansUtil.isGetMethod(name, m)
							|| JavaBeansUtil.isSetMethod(name, m)
							|| JavaBeansUtil.isIsMethod(name, m)) {
						final PropertyDesc propertyDesc = beanDesc
								.getPropertyDesc(JavaBeansUtil.getPropertyName(name));
						addAnnotationDescs(annotations, propertyDesc,
								ElementType.METHOD);
					}
					addAnnotationDescs(annotations, methodDesc,
							ElementType.METHOD);
				}
			}

		};
		final AnnotationDescHandler fieldAnnoHandler = new AnnotationDescHandler() {

			public void handle(BeanDesc beanDesc, Class c) {
				for (Field f : c.getDeclaredFields()) {
					final Annotation[] annotations = f.getDeclaredAnnotations();
					final PropertyDesc pd = beanDesc.getPropertyDesc(f
							.getName());
					addAnnotationDescs(annotations, pd, ElementType.FIELD);
				}
			}

		};
		// TODO constructor, package, and parameters?
		handleAnnotationDesc(this, componentClass, classAnnoHandler,
				methodAnnoHandler, fieldAnnoHandler);
	}

	protected void handleAnnotationDesc(final BeanDesc<T> beanDesc,
			final Class<T> c, final AnnotationDescHandler... handlers) {
		for (AnnotationDescHandler handler : handlers) {
			handler.handle(beanDesc, c);
		}
	}

	protected void addAnnotationDescs(final Annotation[] annotations,
			final Desc desc, final ElementType type) {
		if (desc == null) {
			return;
		}
		if (annotations != null && annotations.length > 0) {
			for (Annotation a : annotations) {
				AnnotationDesc aDesc = createAnnotationDesc(desc, a, type);
				addAnnotationDesc(desc, aDesc);
			}
		}
	}

	protected AnnotationDesc createAnnotationDesc(final Desc desc,
			final Annotation a, final ElementType type) {
		return new AnnotationDescImpl(desc, a, type);
	};

	public void addAnnotationDesc(Desc desc, AnnotationDesc annotationDesc) {
		AssertionUtil.assertNotNull(desc);
		AssertionUtil.assertNotNull(annotationDesc);
		final ElementType type = annotationDesc.getType();
		Map<Desc, List<AnnotationDesc>> map = annotationDescMap.get(type);
		if (map == null) {
			map = new HashMap<Desc, List<AnnotationDesc>>();
		}
		List<AnnotationDesc> list = map.get(desc);
		if (list == null) {
			list = new ArrayList<AnnotationDesc>();
		}
		list.add(annotationDesc);
		map.put(desc, list);
		annotationDescMap.put(type, map);
	}

	protected void analyzeClasses(final Class<T> componentClass) {
		classDesc = new ClassDescImpl<T>(componentClass);
	}

	protected void analyzeConstructors() {
		final ClassDesc<T> classDesc = getClassDesc();
		final ConstructorDesc constructorDesc = new ConstructorDescImpl(
				classDesc);
		setConstructorDesc(constructorDesc);
	}

	public List<AnnotationDesc> getAnnotationDescList(ElementType type,
			Desc desc) {
		final Map<Desc, List<AnnotationDesc>> map = annotationDescMap.get(type);
		if (map != null && map.containsKey(desc)) {
			return map.get(desc);
		}
		return Collections.emptyList();
	}

	public ClassDesc<T> getClassDesc() {
		return classDesc;
	}

	public Class<T> getComponentClass() {
		return classDesc.getComponentClass();
	}

	public MethodDesc getMethodDesc(String methodName) {
		final List<MethodDesc> list = methodDescMap.get(methodName);
		return (list != null && list.size() > 0) ? list.get(0) : null;
	}

	public List<MethodDesc> getMethodDescs(String methodName) {
		return methodDescMap.get(methodName);
	}

	public PropertyDesc getPropertyDesc(String propertyName) {
		return propertyDescMap.get(propertyName);
	}

	public ConstructorDesc getConstructorDesc() {
		return constructorDesc;
	}

	protected void setConstructorDesc(ConstructorDesc constructorDesc) {
		this.constructorDesc = constructorDesc;
	}

	@SuppressWarnings("unchecked")
	public T newInstance(Object... args) {
		final ConstructorDesc cd = getConstructorDesc();
		final Constructor c = cd.getSuitableConstructor(args);
		return (T) ConstructorUtil.newInstance(c, args);
	}

	protected void addMethodDesc(MethodDesc desc) {
		final String methodName = desc.getMethodName();
		if (!methodDescMap.containsKey(methodName)) {
			List<MethodDesc> list = new ArrayList<MethodDesc>();
			list.add(desc);
			methodDescMap.put(methodName, list);
			methodDescList.add(desc);
		} else {
			final List<MethodDesc> list = methodDescMap.get(methodName);
			list.add(desc);
		}
	}

	private static interface AnnotationDescHandler {

		void handle(BeanDesc beanDesc, Class c);
	}

	protected void addPropertyDesc(String propertyName, PropertyDesc propDesc) {
		propertyDescMap.put(propertyName, propDesc);
		propertyDescList.add(propDesc);
	}

	public List<MethodDesc> getAllMethodDesc() {
		return methodDescList;
	}

	public List<PropertyDesc> getAllPropertyDesc() {
		return propertyDescList;
	}
}
